﻿// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.Extensions.Options;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
    public class DefaultPageApplicationModelProviderTest
    {
        [Fact]
        public void OnProvidersExecuting_ThrowsIfPageDoesNotDeriveFromValidBaseType()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(InvalidPageWithWrongBaseClass).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));

            // Assert
            Assert.Equal(
                 $"The type '{typeInfo.FullName}' is not a valid page. A page must inherit from '{typeof(PageBase).FullName}'.",
                 ex.Message);
        }

        private class InvalidPageWithWrongBaseClass : RazorPageBase
        {
            public override void BeginContext(int position, int length, bool isLiteral)
            {
                throw new NotImplementedException();
            }

            public override void EndContext()
            {
                throw new NotImplementedException();
            }

            public override void EnsureRenderedBodyOrSections()
            {
                throw new NotImplementedException();
            }

            public override Task ExecuteAsync()
            {
                throw new NotImplementedException();
            }
        }

        [Fact]
        public void OnProvidersExecuting_ThrowsIfModelPropertyDoesNotExistOnPage()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithoutModelProperty).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));

            // Assert
            Assert.Equal(
                $"The type '{typeInfo.FullName}' is not a valid page. A page must define a public, non-static 'Model' property.",
                ex.Message);
        }

        private class PageWithoutModelProperty : PageBase
        {
            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        [Fact]
        public void OnProvidersExecuting_ThrowsIfModelPropertyIsNotPublic()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithNonVisibleModel).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));

            // Assert
            Assert.Equal(
                $"The type '{typeInfo.FullName}' is not a valid page. A page must define a public, non-static 'Model' property.",
                ex.Message);
        }

        private class PageWithNonVisibleModel : PageBase
        {
            private object Model => null;

            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        [Fact]
        public void OnProvidersExecuting_ThrowsIfModelPropertyIsStatic()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithStaticModel).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));

            // Assert
            Assert.Equal(
                $"The type '{typeInfo.FullName}' is not a valid page. A page must define a public, non-static 'Model' property.",
                ex.Message);
        }

        private class PageWithStaticModel : PageBase
        {
            public static object Model => null;

            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversPropertiesFromPage_IfModelTypeDoesNotHaveAttribute()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithModelWithoutPageModelAttribute).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            var propertiesOnPage = context.PageApplicationModel.HandlerProperties
                .Where(p => p.PropertyInfo.DeclaringType.GetTypeInfo() == typeInfo);
            Assert.Collection(
                propertiesOnPage.OrderBy(p => p.PropertyName),
                property =>
                {
                    Assert.Equal(typeInfo.GetProperty(nameof(PageWithModelWithoutPageModelAttribute.Model)), property.PropertyInfo);
                    Assert.Equal(nameof(PageWithModelWithoutPageModelAttribute.Model), property.PropertyName);
                },
                property =>
                {
                    Assert.Equal(typeInfo.GetProperty(nameof(PageWithModelWithoutPageModelAttribute.Property1)), property.PropertyInfo);
                    Assert.Null(property.BindingInfo);
                    Assert.Equal(nameof(PageWithModelWithoutPageModelAttribute.Property1), property.PropertyName);
                },
                property =>
                {
                    Assert.Equal(typeInfo.GetProperty(nameof(PageWithModelWithoutPageModelAttribute.Property2)), property.PropertyInfo);
                    Assert.Equal(nameof(PageWithModelWithoutPageModelAttribute.Property2), property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                    Assert.Equal(BindingSource.Path, property.BindingInfo.BindingSource);
                });
        }

        private class PageWithModelWithoutPageModelAttribute : Page
        {
            public string Property1 { get; set; }

            [FromRoute]
            public object Property2 { get; set; }

            public ModelWithoutPageModelAttribute Model => null;

            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        private class ModelWithoutPageModelAttribute
        {
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversPropertiesFromPageModel_IfModelHasAttribute()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithModelWithPageModelAttribute).GetTypeInfo();
            var modelType = typeof(ModelWithPageModelAttribute);
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            Assert.Collection(
                context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName),
                property =>
                {
                    Assert.Equal(modelType.GetProperty(nameof(ModelWithPageModelAttribute.Property)), property.PropertyInfo);
                    Assert.Equal(nameof(ModelWithPageModelAttribute.Property), property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                    Assert.Equal(BindingSource.Path, property.BindingInfo.BindingSource);
                });
        }

        private class PageWithModelWithPageModelAttribute : Page
        {
            public string Property1 { get; set; }

            [FromRoute]
            public object Property2 { get; set; }

            public ModelWithPageModelAttribute Model => null;
            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        [PageModel]
        private class ModelWithPageModelAttribute
        {
            [FromRoute]
            public string Property { get; set; }
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversProperties_FromAllSubTypesThatDeclaresBindProperty()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(BindPropertyAttributeOnBaseModelPage).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            Assert.Collection(
                context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName).Where(p => p.BindingInfo != null),
                property =>
                {
                    var name = nameof(ModelLevel3.Property2);
                    Assert.Equal(typeof(ModelLevel3).GetProperty(name), property.PropertyInfo);
                    Assert.Equal(name, property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                },
                property =>
                {
                    var name = nameof(ModelLevel3.Property3);
                    Assert.Equal(typeof(ModelLevel3).GetProperty(name), property.PropertyInfo);
                    Assert.Equal(name, property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                });
        }

        private class BindPropertyAttributeOnBaseModelPage : Page
        {
            public ModelLevel3 Model => null;
            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        private class ModelLevel1 : PageModel
        {
            public string Property1 { get; set; }
        }

        [BindProperties]
        private class ModelLevel2 : ModelLevel1
        {
            public string Property2 { get; set; }
        }

        private class ModelLevel3 : ModelLevel2
        {
            public string Property3 { get; set; }
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversHandlersFromPage()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithModelWithoutHandlers).GetTypeInfo();
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            Assert.Collection(
                context.PageApplicationModel.HandlerMethods.OrderBy(p => p.Name),
                handler =>
                {
                    var name = nameof(PageWithModelWithoutHandlers.OnGet);
                    Assert.Equal(typeInfo.GetMethod(name), handler.MethodInfo);
                    Assert.Equal(name, handler.Name);
                    Assert.Equal("Get", handler.HttpMethod);
                    Assert.Null(handler.HandlerName);
                },
                handler =>
                {
                    var name = nameof(PageWithModelWithoutHandlers.OnPostAsync);
                    Assert.Equal(typeInfo.GetMethod(name), handler.MethodInfo);
                    Assert.Equal(name, handler.Name);
                    Assert.Equal("Post", handler.HttpMethod);
                    Assert.Null(handler.HandlerName);
                },
                handler =>
                {
                    var name = nameof(PageWithModelWithoutHandlers.OnPostDeleteCustomerAsync);
                    Assert.Equal(typeInfo.GetMethod(name), handler.MethodInfo);
                    Assert.Equal(name, handler.Name);
                    Assert.Equal("Post", handler.HttpMethod);
                    Assert.Equal("DeleteCustomer", handler.HandlerName);
                });
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversPropertiesFromModel()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithModel).GetTypeInfo();
            var modelType = typeof(TestPageModel);
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            Assert.Collection(
                context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName),
                property =>
                {
                    var name = nameof(TestPageModel.Property1);
                    Assert.Equal(modelType.GetProperty(name), property.PropertyInfo);
                    Assert.Null(property.BindingInfo);
                    Assert.Equal(name, property.PropertyName);
                },
                property =>
                {
                    var name = nameof(TestPageModel.Property2);
                    Assert.Equal(modelType.GetProperty(name), property.PropertyInfo);
                    Assert.Equal(name, property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                    Assert.Equal(BindingSource.Query, property.BindingInfo.BindingSource);
                });
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversBindingInfoFromHandler()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithBindPropertyModel).GetTypeInfo();
            var modelType = typeof(ModelWithBindProperty);
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            Assert.Collection(
                context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName),
                property =>
                {
                    Assert.Equal(nameof(ModelWithBindProperty.Property1), property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                },
                property =>
                {
                    Assert.Equal(nameof(ModelWithBindProperty.Property2), property.PropertyName);
                    Assert.NotNull(property.BindingInfo);
                    Assert.Equal(BindingSource.Path, property.BindingInfo.BindingSource);
                });
        }

        private class PageWithBindPropertyModel : PageBase
        {
            public ModelWithBindProperty Model => null;

            public override Task ExecuteAsync() => null;
        }

        [BindProperties]
        [PageModel]
        private class ModelWithBindProperty
        {
            public string Property1 { get; set; }

            [FromRoute]
            public string Property2 { get; set; }
        }

        [Fact]
        public void OnProvidersExecuting_DiscoversHandlersFromModel()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithModel).GetTypeInfo();
            var modelType = typeof(TestPageModel);
            var descriptor = new PageActionDescriptor();
            var context = new PageApplicationModelProviderContext(descriptor, typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            Assert.NotNull(context.PageApplicationModel);
            Assert.Collection(
                context.PageApplicationModel.HandlerMethods.OrderBy(p => p.Name),
                handler =>
                {
                    var name = nameof(TestPageModel.OnGetUser);
                    Assert.Equal(modelType.GetMethod(name), handler.MethodInfo);
                    Assert.Equal(name, handler.Name);
                    Assert.Equal("Get", handler.HttpMethod);
                    Assert.Equal("User", handler.HandlerName);
                });
        }

        // We want to test the 'empty' page has no bound properties, and no handler methods.
        [Fact]
        public void OnProvidersExecuting_EmptyPage()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(EmptyPage).GetTypeInfo();
            var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var pageModel = context.PageApplicationModel;
            Assert.Empty(pageModel.HandlerProperties.Where(p => p.BindingInfo != null));
            Assert.Empty(pageModel.HandlerMethods);
            Assert.Same(typeof(EmptyPage).GetTypeInfo(), pageModel.HandlerType);
            Assert.Same(typeof(EmptyPage).GetTypeInfo(), pageModel.ModelType);
            Assert.Same(typeof(EmptyPage).GetTypeInfo(), pageModel.PageType);
        }

        // We want to test the 'empty' page and PageModel has no bound properties, and no handler methods.
        [Fact]
        public void OnProvidersExecuting_EmptyPageModel()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(EmptyPageWithPageModel).GetTypeInfo();
            var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var pageModel = context.PageApplicationModel;
            Assert.Empty(pageModel.HandlerProperties.Where(p => p.BindingInfo != null));
            Assert.Empty(pageModel.HandlerMethods);
            Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.DeclaredModelType);
            Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.ModelType);
            Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.HandlerType);
            Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), pageModel.PageType);
        }

        private class EmptyPage : Page
        {
            // Copied from generated code
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<EmptyPage> Html { get; private set; }
            public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary<EmptyPage> ViewData => null;
            public EmptyPage Model => ViewData.Model;

            public override Task ExecuteAsync()
            {
                throw new NotImplementedException();
            }
        }

        private class EmptyPageWithPageModel : Page
        {
            // Copied from generated code
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; }
            [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
            public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<EmptyPageModel> Html { get; private set; }
            public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary<EmptyPageModel> ViewData => null;
            public EmptyPageModel Model => ViewData.Model;

            public override Task ExecuteAsync()
            {
                throw new NotImplementedException();
            }
        }

        private class EmptyPageModel : PageModel
        {
        }

        [Fact] // If the model has handler methods, we prefer those.
        public void CreateDescriptor_FindsHandlerMethod_OnModel()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo();
            var modelType = typeof(ModelWithHandler);
            var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var pageModel = context.PageApplicationModel;
            Assert.Contains(
                pageModel.HandlerProperties,
                p => p.PropertyInfo == modelType.GetProperty(nameof(ModelWithHandler.BindMe)));

            Assert.Collection(
                pageModel.HandlerMethods,
                p => Assert.Equal(modelType.GetMethod(nameof(ModelWithHandler.OnGet)), p.MethodInfo));

            Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), pageModel.HandlerType);
            Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), pageModel.ModelType);
            Assert.Same(typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(), pageModel.PageType);
        }

        private class ModelWithHandler : PageModel
        {
            [ModelBinder]
            public int BindMe { get; set; }

            public void OnGet() { }
        }

        private class PageWithHandlerThatGetsIgnored : Page
        {
            public ModelWithHandler Model => null;

            [ModelBinder]
            public int IgnoreMe { get; set; }

            public void OnPost() { }

            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        [Fact] // If the model does not have the PageModelAttribute, we look at the page instead.
        public void OnProvidersExecuting_FindsHandlerMethodOnPage_WhenModelIsNotAnnotatedWithPageModelAttribute()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithHandler).GetTypeInfo();
            var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo);

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var pageModel = context.PageApplicationModel;
            var propertiesOnPage = pageModel.HandlerProperties
                .Where(p => p.PropertyInfo.DeclaringType.GetTypeInfo() == typeInfo);
            Assert.Collection(
                propertiesOnPage.OrderBy(p => p.PropertyName),
                p => Assert.Equal(typeInfo.GetProperty(nameof(PageWithHandler.BindMe)), p.PropertyInfo),
                p => Assert.Equal(typeInfo.GetProperty(nameof(PageWithHandler.Model)), p.PropertyInfo));

            Assert.Collection(
                pageModel.HandlerMethods,
                p => Assert.Equal(typeInfo.GetMethod(nameof(PageWithHandler.OnGet)), p.MethodInfo));

            Assert.Same(typeof(PageWithHandler).GetTypeInfo(), pageModel.HandlerType);
            Assert.Same(typeof(PocoModel).GetTypeInfo(), pageModel.ModelType);
            Assert.Same(typeof(PageWithHandler).GetTypeInfo(), pageModel.PageType);
        }

        private class PageWithHandler : Page
        {
            public PocoModel Model => null;

            [ModelBinder]
            public int BindMe { get; set; }

            public void OnGet() { }

            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        private class PocoModel
        {
            // Just a plain ol' model, nothing to see here.

            [ModelBinder]
            public int IgnoreMe { get; set; }

            public void OnGet() { }
        }

        [Fact]
        public void PopulateHandlerMethods_DiscoversHandlersFromBaseType()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(InheritsMethods).GetTypeInfo();
            var baseType = typeof(TestSetPageModel);
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Collection(
                handlerMethods.OrderBy(h => h.MethodInfo.DeclaringType.Name).ThenBy(h => h.MethodInfo.Name),
                handler =>
                {
                    Assert.Equal(nameof(InheritsMethods.OnGet), handler.MethodInfo.Name);
                    Assert.Equal(typeInfo, handler.MethodInfo.DeclaringType.GetTypeInfo());
                },
                handler =>
                {
                    Assert.Equal(nameof(TestSetPageModel.OnGet), handler.MethodInfo.Name);
                    Assert.Equal(baseType, handler.MethodInfo.DeclaringType);
                },
                handler =>
                {
                    Assert.Equal(nameof(TestSetPageModel.OnPost), handler.MethodInfo.Name);
                    Assert.Equal(baseType, handler.MethodInfo.DeclaringType);
                });
        }

        private class TestSetPageModel
        {
            public void OnGet()
            {
            }

            public void OnPost()
            {
            }
        }

        private class InheritsMethods : TestSetPageModel
        {
            public new void OnGet()
            {
            }
        }

        [Fact]
        public void PopulateHandlerMethods_IgnoresNonPublicMethods()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(ProtectedModel).GetTypeInfo();
            var baseType = typeof(TestSetPageModel);
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Empty(handlerMethods);
        }

        private class ProtectedModel
        {
            protected void OnGet()
            {
            }

            private void OnPost()
            {
            }
        }

        [Fact]
        public void PopulateHandlerMethods_IgnoreGenericTypeParameters()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(GenericClassModel).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Empty(handlerMethods);
        }

        private class GenericClassModel
        {
            public void OnGet<T>()
            {
            }
        }

        [Fact]
        public void PopulateHandlerMethods_IgnoresStaticMethods()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageModelWithStaticHandler).GetTypeInfo();
            var expected = typeInfo.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance);
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Collection(
                handlerMethods,
                handler => Assert.Same(expected, handler.MethodInfo));
        }

        private class PageModelWithStaticHandler
        {
            public static void OnGet(string name)
            {
            }

            public void OnGet()
            {
            }
        }

        [Fact]
        public void PopulateHandlerMethods_IgnoresAbstractMethods()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageModelWithAbstractMethod).GetTypeInfo();
            var expected = typeInfo.GetMethod(nameof(PageModelWithAbstractMethod.OnGet), BindingFlags.Public | BindingFlags.Instance);
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Collection(
                handlerMethods,
                handler => Assert.Same(expected, handler.MethodInfo));
        }

        private abstract class PageModelWithAbstractMethod
        {
            public abstract void OnPost(string name);

            public void OnGet()
            {
            }
        }

        [Fact]
        public void PopulateHandlerMethods_IgnoresMethodWithNonHandlerAttribute()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithNonHandlerMethod).GetTypeInfo();
            var expected = typeInfo.GetMethod(nameof(PageWithNonHandlerMethod.OnGet), BindingFlags.Public | BindingFlags.Instance);
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Collection(
                handlerMethods,
                handler => Assert.Same(expected, handler.MethodInfo));
        }

        private class PageWithNonHandlerMethod
        {
            [NonHandler]
            public void OnPost(string name) { }

            public void OnGet()
            {
            }
        }

        // There are more tests for the parsing elsewhere, this is just testing that it's wired
        // up to the model.
        [Fact]
        public void CreateHandlerModel_ParsesMethod()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageModelWithHandlerNames).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            Assert.Collection(
                handlerMethods.OrderBy(h => h.MethodInfo.Name),
                handler =>
                {
                    Assert.Same(typeInfo.GetMethod(nameof(PageModelWithHandlerNames.OnPutDeleteAsync)), handler.MethodInfo);
                    Assert.Equal("Put", handler.HttpMethod);
                    Assert.Equal("Delete", handler.HandlerName);
                });
        }

        private class PageModelWithHandlerNames
        {
            public void OnPutDeleteAsync()
            {
            }

            public void Foo() // This isn't a valid handler name.
            {
            }
        }

        [Fact]
        public void CreateHandlerMethods_AddsParameterDescriptors()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(PageWithHandlerParameters).GetTypeInfo();
            var expected = typeInfo.GetMethod(nameof(PageWithHandlerParameters.OnPost));
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            var handler = Assert.Single(handlerMethods);

            Assert.Collection(
                handler.Parameters,
                p =>
                {
                    Assert.NotNull(p.ParameterInfo);
                    Assert.Equal(typeof(string), p.ParameterInfo.ParameterType);
                    Assert.Equal("name", p.ParameterName);
                },
                p =>
                {
                    Assert.NotNull(p.ParameterInfo);
                    Assert.Equal(typeof(int), p.ParameterInfo.ParameterType);
                    Assert.Equal("id", p.ParameterName);
                    Assert.Equal("personId", p.BindingInfo.BinderModelName);
                });
        }

        [Fact]
        public void CreateHandlerMethods_WithLegacyValidationBehavior_AddsParameterDescriptors()
        {
            // Arrange
            var provider = new DefaultPageApplicationModelProvider(
                TestModelMetadataProvider.CreateDefaultProvider(),
                Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = false }),
                Options.Create(new RazorPagesOptions()));
            var typeInfo = typeof(PageWithHandlerParameters).GetTypeInfo();
            var expected = typeInfo.GetMethod(nameof(PageWithHandlerParameters.OnPost));
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerMethods(pageModel);

            // Assert
            var handlerMethods = pageModel.HandlerMethods;
            var handler = Assert.Single(handlerMethods);

            Assert.Collection(
                handler.Parameters,
                p =>
                {
                    Assert.NotNull(p.ParameterInfo);
                    Assert.Equal(typeof(string), p.ParameterInfo.ParameterType);
                    Assert.Equal("name", p.ParameterName);
                },
                p =>
                {
                    Assert.NotNull(p.ParameterInfo);
                    Assert.Equal(typeof(int), p.ParameterInfo.ParameterType);
                    Assert.Equal("id", p.ParameterName);
                    Assert.Equal("personId", p.BindingInfo.BinderModelName);
                });
        }

        private class PageWithHandlerParameters
        {
            public void OnPost(string name, [ModelBinder(Name = "personId")] int id) { }
        }

        // We're using PropertyHelper from Common to find the properties here, which implements
        // out standard set of semantics for properties that the framework interacts with.
        //
        // One of the desirable consequences of that is we only find 'visible' properties. We're not
        // retesting all of the details of PropertyHelper here, just the visibility part as a quick check
        // that we're using PropertyHelper as expected.
        [Fact]
        public void PopulateHandlerProperties_UsesPropertyHelpers_ToFindProperties()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(HidesAProperty).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerProperties(pageModel);

            // Assert
            var properties = pageModel.HandlerProperties;
            Assert.Collection(
                properties,
                p =>
                {
                    Assert.Equal(typeof(HidesAProperty).GetTypeInfo(), p.PropertyInfo.DeclaringType.GetTypeInfo());
                });
        }

        private class HasAHiddenProperty
        {
            [BindProperty]
            public int Property { get; set; }
        }

        private class HidesAProperty : HasAHiddenProperty
        {
            [BindProperty]
            public new int Property { get; set; }
        }

        [Fact]
        public void PopulateHandlerProperties_SupportsGet_OnProperty()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(ModelSupportsGetOnProperty).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]);

            // Act
            provider.PopulateHandlerProperties(pageModel);

            // Assert
            var properties = pageModel.HandlerProperties;
            Assert.Collection(
                properties.OrderBy(p => p.PropertyName),
                p =>
                {
                    Assert.Equal(typeInfo.GetProperty(nameof(ModelSupportsGetOnProperty.Property)), p.PropertyInfo);
                    Assert.NotNull(p.BindingInfo.RequestPredicate);
                    Assert.True(p.BindingInfo.RequestPredicate(new ActionContext
                    {
                        HttpContext = new DefaultHttpContext
                        {
                            Request =
                            {
                                Method ="GET",
                            }
                        }
                    }));
                });
        }

        private class ModelSupportsGetOnProperty
        {
            [BindProperty(SupportsGet = true)]
            public int Property { get; set; }
        }

        [Theory]
        [InlineData("Foo")]
        [InlineData("On")]
        [InlineData("OnAsync")]
        [InlineData("Async")]
        public void TryParseHandler_ParsesHandlerNames_InvalidData(string methodName)
        {
            // Act
            var result = DefaultPageApplicationModelProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler);

            // Assert
            Assert.False(result);
            Assert.Null(httpMethod);
            Assert.Null(handler);
        }

        [Theory]
        [InlineData("OnG", "G", null)]
        [InlineData("OnGAsync", "G", null)]
        [InlineData("OnPOST", "P", "OST")]
        [InlineData("OnPOSTAsync", "P", "OST")]
        [InlineData("OnDeleteFoo", "Delete", "Foo")]
        [InlineData("OnDeleteFooAsync", "Delete", "Foo")]
        [InlineData("OnMadeupLongHandlerName", "Madeup", "LongHandlerName")]
        [InlineData("OnMadeupLongHandlerNameAsync", "Madeup", "LongHandlerName")]
        public void TryParseHandler_ParsesHandlerNames_ValidData(string methodName, string expectedHttpMethod, string expectedHandler)
        {
            // Arrange

            // Act
            var result = DefaultPageApplicationModelProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler);

            // Assert
            Assert.True(result);
            Assert.Equal(expectedHttpMethod, httpMethod);
            Assert.Equal(expectedHandler, handler);
        }

        private class PageWithModelWithoutHandlers : Page
        {
            public ModelWithoutHandler Model { get; }

            public override Task ExecuteAsync() => throw new NotImplementedException();

            public void OnGet() { }

            public void OnPostAsync() { }

            public void OnPostDeleteCustomerAsync() { }

            public class ModelWithoutHandler
            {
            }
        }

        private class PageWithModel : Page
        {
            public TestPageModel Model { get; }

            public override Task ExecuteAsync() => throw new NotImplementedException();
        }

        [PageModel]
        private class TestPageModel
        {
            public string Property1 { get; set; }

            [FromQuery]
            public string Property2 { get; set; }

            public void OnGetUser() { }
        }

        [Fact]
        public void PopulateFilters_With21CompatBehavior_DoesNotAddDisallowOptionsRequestsPageFilter()
        {
            // Arrange
            var provider = new DefaultPageApplicationModelProvider(
                TestModelMetadataProvider.CreateDefaultProvider(),
                Options.Create(new MvcOptions()),
                Options.Create(new RazorPagesOptions()));
            var typeInfo = typeof(object).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true));

            // Act
            provider.PopulateFilters(pageModel);

            // Assert
            Assert.Empty(pageModel.Filters);
        }

        [Fact]
        public void PopulateFilters_AddsDisallowOptionsRequestsPageFilter()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(object).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true));

            // Act
            provider.PopulateFilters(pageModel);

            // Assert
            Assert.Collection(
                pageModel.Filters,
                filter => Assert.IsType<HandleOptionsRequestsPageFilter>(filter));
        }

        [Fact]
        public void PopulateFilters_AddsIFilterMetadataAttributesToModel()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(FilterModel).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true));

            // Act
            provider.PopulateFilters(pageModel);

            // Assert
            Assert.Collection(
                pageModel.Filters,
                filter => Assert.IsType<TypeFilterAttribute>(filter),
                filter => Assert.IsType<HandleOptionsRequestsPageFilter>(filter));
        }

        [PageModel]
        [Serializable]
        [TypeFilter(typeof(object))]
        private class FilterModel
        {
        }

        [Fact]
        public void PopulateFilters_AddsPageHandlerPageFilter_IfPageImplementsIAsyncPageFilter()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(ModelImplementingAsyncPageFilter).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true));

            // Act
            provider.PopulateFilters(pageModel);

            // Assert
            Assert.Collection(
                pageModel.Filters,
                filter => Assert.IsType<PageHandlerPageFilter>(filter),
                filter => Assert.IsType<HandleOptionsRequestsPageFilter>(filter));
        }

        private class ModelImplementingAsyncPageFilter : IAsyncPageFilter
        {
            public Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
            {
                throw new NotImplementedException();
            }

            public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
            {
                throw new NotImplementedException();
            }
        }

        [Fact]
        public void PopulateFilters_AddsPageHandlerPageFilter_IfPageImplementsIPageFilter()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(ModelImplementingPageFilter).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true));

            // Act
            provider.PopulateFilters(pageModel);

            // Assert
            Assert.Collection(
                pageModel.Filters,
                filter => Assert.IsType<PageHandlerPageFilter>(filter),
                filter => Assert.IsType<HandleOptionsRequestsPageFilter>(filter));
        }

        private class ModelImplementingPageFilter : IPageFilter
        {
            public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
            {
                throw new NotImplementedException();
            }

            public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
            {
                throw new NotImplementedException();
            }

            public void OnPageHandlerSelected(PageHandlerSelectedContext context)
            {
                throw new NotImplementedException();
            }
        }

        [Fact]
        public void PopulateFilters_AddsPageHandlerPageFilter_ForModelDerivingFromTypeImplementingPageFilter()
        {
            // Arrange
            var provider = CreateProvider();
            var typeInfo = typeof(DerivedFromPageModel).GetTypeInfo();
            var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true));

            // Act
            provider.PopulateFilters(pageModel);

            // Assert
            Assert.Collection(
                pageModel.Filters,
                filter => Assert.IsType<ServiceFilterAttribute>(filter),
                filter => Assert.IsType<PageHandlerPageFilter>(filter),
                filter => Assert.IsType<HandleOptionsRequestsPageFilter>(filter));
        }

        [ServiceFilter(typeof(IServiceProvider))]
        private class DerivedFromPageModel : PageModel { }

        private static DefaultPageApplicationModelProvider CreateProvider()
        {
            return new DefaultPageApplicationModelProvider(
                TestModelMetadataProvider.CreateDefaultProvider(),
                Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true }),
                Options.Create(new RazorPagesOptions {  AllowDefaultHandlingForOptionsRequests = true }));
        }
    }
}
